diff --git a/lib/platform/gitlab/index.js b/lib/platform/gitlab/index.js
index 952c31b08c4bca1f103b967c3c8d1956097f622e..11d036d6fe0d5163fa6dcd25f73662a4ffa9fca0 100644
--- a/lib/platform/gitlab/index.js
+++ b/lib/platform/gitlab/index.js
@@ -381,12 +381,97 @@ function addReviewers(iid, reviewers) {
   logger.warn('Unimplemented in GitLab: approvals');
 }
 
-async function ensureComment() {
-  // Todo: implement. See GitHub API for example
+async function getComments(issueNo) {
+  // GET https://gitlab.com/api/v4/projects/:owner/:repo/merge_requests/:number/notes
+  logger.debug(`Getting comments for #${issueNo}`);
+  const url = `https://gitlab.com/api/v4/projects/${
+    config.repository
+  }/merge_requests/${issueNo}/notes`;
+  const comments = (await get(url, { paginate: true })).body;
+  logger.debug(`Found ${comments.length} comments`);
+  return comments;
+}
+
+async function addComment(issueNo, body) {
+  // POST https://gitlab.com/api/v4/projects/:owner/:repo/merge_requests/:number/notes
+  await get.post(
+    `https://gitlab.com/api/v4/projects/${
+      config.repository
+    }/merge_requests/${issueNo}/notes`,
+    {
+      body: { body },
+    }
+  );
+}
+
+async function editComment(issueNo, commentId, body) {
+  // PATCH https://gitlab.com/api/v4/projects/:owner/:repo/merge_requests/:number/notes/:id
+  await get.patch(
+    `https://gitlab.com/api/v4/projects/${
+      config.repository
+    }/merge_requests/${issueNo}/notes/${commentId}`,
+    {
+      body: { body },
+    }
+  );
+}
+
+async function deleteComment(issueNo, commentId) {
+  // DELETE https://gitlab.com/api/v4/projects/:owner/:repo/merge_requests/:number/notes/:id
+  await get.delete(
+    `https://gitlab.com/api/v4/projects/${
+      config.repository
+    }/merge_requests/${issueNo}/notes/${commentId}`
+  );
 }
 
-async function ensureCommentRemoval() {
-  // Todo: implement. See GitHub API for example
+async function ensureComment(issueNo, topic, content) {
+  const comments = await getComments(issueNo);
+  let body;
+  let commentId;
+  let commentNeedsUpdating;
+  if (topic) {
+    logger.debug(`Ensuring comment "${topic}" in #${issueNo}`);
+    body = `### ${topic}\n\n${content}`;
+    comments.forEach(comment => {
+      if (comment.body.startsWith(`### ${topic}\n\n`)) {
+        commentId = comment.id;
+        commentNeedsUpdating = comment.body !== body;
+      }
+    });
+  } else {
+    logger.debug(`Ensuring content-only comment in #${issueNo}`);
+    body = `${content}`;
+    comments.forEach(comment => {
+      if (comment.body === body) {
+        commentId = comment.id;
+        commentNeedsUpdating = false;
+      }
+    });
+  }
+  if (!commentId) {
+    await addComment(issueNo, body);
+    logger.info({ repository: config.repository, issueNo }, 'Added comment');
+  } else if (commentNeedsUpdating) {
+    await editComment(issueNo, commentId, body);
+    logger.info({ repository: config.repository, issueNo }, 'Updated comment');
+  } else {
+    logger.debug('Comment is already update-to-date');
+  }
+}
+
+async function ensureCommentRemoval(issueNo, topic) {
+  logger.debug(`Ensuring comment "${topic}" in #${issueNo} is removed`);
+  const comments = await getComments(issueNo);
+  let commentId;
+  comments.forEach(comment => {
+    if (comment.body.startsWith(`### ${topic}\n\n`)) {
+      commentId = comment.id;
+    }
+  });
+  if (commentId) {
+    await deleteComment(issueNo, commentId);
+  }
 }
 
 async function getPrList() {
diff --git a/test/platform/gitlab/__snapshots__/index.spec.js.snap b/test/platform/gitlab/__snapshots__/index.spec.js.snap
index e19ed8345f1bef098ebc0f422b9f357a25638005..231763b4939dc2598c692e5fd6ccd4ab41dbf79e 100644
--- a/test/platform/gitlab/__snapshots__/index.spec.js.snap
+++ b/test/platform/gitlab/__snapshots__/index.spec.js.snap
@@ -124,6 +124,38 @@ Array [
 ]
 `;
 
+exports[`platform/gitlab ensureComment add comment if not found 1`] = `
+Array [
+  Array [
+    "https://gitlab.com/api/v4/projects/some%2Frepo/merge_requests/42/notes",
+    Object {
+      "body": Object {
+        "body": "### some-subject
+
+some
+content",
+      },
+    },
+  ],
+]
+`;
+
+exports[`platform/gitlab ensureComment add updates comment if necessary 1`] = `
+Array [
+  Array [
+    "https://gitlab.com/api/v4/projects/some%2Frepo/merge_requests/42/notes/1234",
+    Object {
+      "body": Object {
+        "body": "### some-subject
+
+some
+content",
+      },
+    },
+  ],
+]
+`;
+
 exports[`platform/gitlab getAllRenovateBranches() should return all renovate branches 1`] = `
 Array [
   "renovate/a",
diff --git a/test/platform/gitlab/index.spec.js b/test/platform/gitlab/index.spec.js
index 758131f3a32de9b66a5d7dbac1f34fb4fb09811f..3dbe45b825e63ff02940ea30d9a92fbefb6abc22 100644
--- a/test/platform/gitlab/index.spec.js
+++ b/test/platform/gitlab/index.spec.js
@@ -78,6 +78,16 @@ describe('platform/gitlab', () => {
         email: 'a@b.com',
       },
     }));
+    get.mockReturnValue({
+      body: [
+        {
+          number: 1,
+          source_branch: 'branch-a',
+          title: 'branch a pr',
+          state: 'opened',
+        },
+      ],
+    });
     return gitlab.initRepo(...args);
   }
 
@@ -529,13 +539,48 @@ describe('platform/gitlab', () => {
     });
   });
   describe('ensureComment', () => {
-    it('exists', async () => {
+    it('add comment if not found', async () => {
+      await initRepo({ repository: 'some/repo', token: 'token' });
+      get.mockReturnValueOnce({ body: [] });
+      await gitlab.ensureComment(42, 'some-subject', 'some\ncontent');
+      expect(get.post.mock.calls).toHaveLength(1);
+      expect(get.post.mock.calls).toMatchSnapshot();
+    });
+    it('add updates comment if necessary', async () => {
+      await initRepo({ repository: 'some/repo', token: 'token' });
+      get.mockReturnValueOnce({
+        body: [{ id: 1234, body: '### some-subject\n\nblablabla' }],
+      });
+      await gitlab.ensureComment(42, 'some-subject', 'some\ncontent');
+      expect(get.post.mock.calls).toHaveLength(0);
+      expect(get.patch.mock.calls).toHaveLength(1);
+      expect(get.patch.mock.calls).toMatchSnapshot();
+    });
+    it('skips comment', async () => {
+      await initRepo({ repository: 'some/repo', token: 'token' });
+      get.mockReturnValueOnce({
+        body: [{ id: 1234, body: '### some-subject\n\nsome\ncontent' }],
+      });
       await gitlab.ensureComment(42, 'some-subject', 'some\ncontent');
+      expect(get.post.mock.calls).toHaveLength(0);
+      expect(get.patch.mock.calls).toHaveLength(0);
+    });
+    it('handles comment with no description', async () => {
+      await initRepo({ repository: 'some/repo', token: 'token' });
+      get.mockReturnValueOnce({ body: [{ id: 1234, body: '!merge' }] });
+      await gitlab.ensureComment(42, null, '!merge');
+      expect(get.post.mock.calls).toHaveLength(0);
+      expect(get.patch.mock.calls).toHaveLength(0);
     });
   });
   describe('ensureCommentRemoval', () => {
-    it('exists', async () => {
+    it('deletes comment if found', async () => {
+      await initRepo({ repository: 'some/repo', token: 'token' });
+      get.mockReturnValueOnce({
+        body: [{ id: 1234, body: '### some-subject\n\nblablabla' }],
+      });
       await gitlab.ensureCommentRemoval(42, 'some-subject');
+      expect(get.delete.mock.calls).toHaveLength(1);
     });
   });
   describe('findPr(branchName, prTitle, state)', () => {