diff --git a/CHANGELOG b/CHANGELOG
index 2dd80779541289bdadf32c953ec7eeff5607c01b..71964051bb19e45f01d08abf5f2d012a015de68b 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -100,6 +100,10 @@ v 8.7.1
   - Prevent users from deleting Webhooks via API they do not own
   - Fix Error 500 due to stale cache when projects are renamed or transferred
   - Update width of search box to fix Safari bug. !3900 (Jedidiah)
+  - Added inline diff styling for `change_title` system notes. !3576 (Adam Butler)
+  - Added `InlineDiffFilter` to the markdown parser. !3576 (Adam Butler)
+
+v 8.7.1 (unreleased)
   - Use the `can?` helper instead of `current_user.can?`
 
 v 8.7.0
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 61d9954c6c87dfafba5664431fe1c516004f9e31..8c96c7a9c31e8c554545a9a1c0776ceacb4ad3bc 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -36,22 +36,6 @@
       }
     }
 
-    .filename {
-      &.old {
-        display: inline-block;
-        span.idiff {
-          background-color: #f8cbcb;
-        }
-      }
-
-      &.new {
-        display: inline-block;
-        span.idiff {
-          background-color: #a6f3a6;
-        }
-      }
-    }
-
     a:not(.btn) {
       color: $gl-dark-link-color;
     }
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 2779cd56788bd069a2964d747cf3df3a2672ae7c..487081c3c7cdb606067ec2323babfd5ba31bc801 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -269,3 +269,11 @@ h1, h2, h3, h4 {
     text-align: right;
   }
 }
+
+.idiff.deletion {
+  background: $gl-idiff-deletion;
+}
+
+.idiff.addition {
+  background: $gl-idiff-addition;
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 5fa4c2666077c2ad3ae154975b3ab84d9e94a1d7..b7d8f61162131e3e7db277a72cf28246c382dc35 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -178,6 +178,9 @@ $table-border-gray: #f0f0f0;
 $line-target-blue: #eaf3fc;
 $line-select-yellow: #fcf8e7;
 $line-select-yellow-dark: #f0e2bd;
+$gl-idiff-deletion: #f8cbcb;
+$gl-idiff-addition: #a6f3a6;
+
 /*
  * Fonts
  */
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 5f311f3780a24c8ffd0606a115684cefef1d0cd8..ea383f9b0f6fe68cb709b909d5f33b7d50b23c48 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -2,8 +2,8 @@ module DiffHelper
   def mark_inline_diffs(old_line, new_line)
     old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs
 
-    marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs)
-    marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs)
+    marked_old_line = Gitlab::Diff::InlineDiffMarker.new(old_line).mark(old_diffs, mode: :deletion)
+    marked_new_line = Gitlab::Diff::InlineDiffMarker.new(new_line).mark(new_diffs, mode: :addition)
 
     [marked_old_line, marked_new_line]
   end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 4bdb1b0c0747fe1a1dedabf45aa0e95b538f1438..758abd7f547b58c8f081ad060e165a62a10bee04 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -171,7 +171,14 @@ class SystemNoteService
   def self.change_title(noteable, project, author, old_title)
     return unless noteable.respond_to?(:title)
 
-    body = "Title changed from **#{old_title}** to **#{noteable.title}**"
+    new_title = noteable.title.dup
+
+    old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs
+
+    marked_old_title = Gitlab::Diff::InlineDiffMarker.new(old_title).mark(old_diffs, mode: :deletion, markdown: true)
+    marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true)
+
+    body = "Changed title: **#{marked_old_title}** → **#{marked_new_title}**"
     create_note(noteable: noteable, project: project, author: author, note: body)
   end
 
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 0f04fc5d33c50cb0d0da62e4c4348932e68e9acf..7194d9c3ebb7e088e2cdd88b8ec48651c398824a 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -11,11 +11,9 @@
       = link_to "#diff-#{i}" do
         - if diff_file.renamed_file
           - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
-          .filename.old
-            = old_path
+          = old_path
           →
-          .filename.new
-            = new_path
+          = new_path
         - else
           %span
             = diff_file.new_path
diff --git a/lib/banzai/filter/inline_diff_filter.rb b/lib/banzai/filter/inline_diff_filter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9e75edd4d4c46e48cb90e8176fc19a377f6ffdf6
--- /dev/null
+++ b/lib/banzai/filter/inline_diff_filter.rb
@@ -0,0 +1,22 @@
+module Banzai
+  module Filter
+    class InlineDiffFilter < HTML::Pipeline::Filter
+      IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
+
+      def call
+        search_text_nodes(doc).each do |node|
+          next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
+
+          content = node.to_html
+          content = content.gsub(/(?:\[\-(.*?)\-\]|\{\-(.*?)\-\})/, '<span class="idiff left right deletion">\1\2</span>')
+          content = content.gsub(/(?:\[\+(.*?)\+\]|\{\+(.*?)\+\})/, '<span class="idiff left right addition">\1\2</span>')
+
+          next if html == content
+
+          node.replace(content)
+        end
+        doc
+      end
+    end
+  end
+end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index ed3cfd6b02323d8ad030f0ea2d1a094320f08c15..b27ecf3c923a95ab116abdc75474ef8ca01e7ccc 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -23,7 +23,8 @@ module Banzai
           Filter::LabelReferenceFilter,
           Filter::MilestoneReferenceFilter,
 
-          Filter::TaskListFilter
+          Filter::TaskListFilter,
+          Filter::InlineDiffFilter
         ]
       end
 
diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb
index dccb717e95dfbfb3a535221301204205096cab13..c9601d42d500b026ae53885318a44c1c8ade99ad 100644
--- a/lib/gitlab/diff/inline_diff_marker.rb
+++ b/lib/gitlab/diff/inline_diff_marker.rb
@@ -1,6 +1,11 @@
 module Gitlab
   module Diff
     class InlineDiffMarker
+      MARKDOWN_SYMBOLS = {
+        addition: "+",
+        deletion: "-"
+      }
+
       attr_accessor :raw_line, :rich_line
 
       def initialize(raw_line, rich_line = raw_line)
@@ -8,7 +13,7 @@ module Gitlab
         @rich_line = ERB::Util.html_escape(rich_line)
       end
 
-      def mark(line_inline_diffs)
+      def mark(line_inline_diffs, mode: nil, markdown: false)
         return rich_line unless line_inline_diffs
 
         marker_ranges = []
@@ -20,13 +25,12 @@ module Gitlab
         end
 
         offset = 0
-        # Mark each range
-        marker_ranges.each_with_index do |range, i|
-          class_names = ["idiff"]
-          class_names << "left"   if i == 0
-          class_names << "right"  if i == marker_ranges.length - 1
 
-          offset = insert_around_range(rich_line, range, "<span class='#{class_names.join(" ")}'>", "</span>", offset)
+        # Mark each range
+        marker_ranges.each_with_index do |range, index|
+          before_content = markdown ? "{#{MARKDOWN_SYMBOLS[mode]}" : "<span class='#{html_class_names(marker_ranges, mode, index)}'>"
+          after_content = markdown ? "#{MARKDOWN_SYMBOLS[mode]}}" : "</span>"
+          offset = insert_around_range(rich_line, range, before_content, after_content, offset)
         end
 
         rich_line.html_safe
@@ -34,6 +38,14 @@ module Gitlab
 
       private
 
+      def html_class_names(marker_ranges, mode, index)
+        class_names = ["idiff"]
+        class_names << "left"  if index == 0
+        class_names << "right" if index == marker_ranges.length - 1
+        class_names << mode if mode
+        class_names.join(" ")
+      end
+
       # Mapping of character positions in the raw line, to the rich (highlighted) line
       def position_mapping
         @position_mapping ||= begin
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index 0148c87084a6bc449157094598b2f07bde965376..1d892fe1a55b85e4686cd6c3956bbafc78f8c06e 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -278,6 +278,10 @@ describe 'GitLab Markdown', feature: true do
     it 'includes GollumTagsFilter' do
       expect(doc).to parse_gollum_tags
     end
+
+    it 'includes InlineDiffFilter' do
+      expect(doc).to parse_inline_diffs
+    end
   end
 
   # Fake a `current_user` helper
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 1772cc3f6a4a30d7ea520b8fff466e94d8a70191..92c7fdf27bdcbab49cc8cb5331b97563b5dd4084 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -239,3 +239,16 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
 - [[link-text|http://example.com/pdfs/gollum.pdf]]
 - [[images/example.jpg]]
 - [[http://example.com/images/example.jpg]]
+
+### Inline Diffs
+
+With inline diffs tags you can display {+ additions +} or [- deletions -].
+
+The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}.
+
+However the wrapping tags can not be mixed as such -
+
+- {+ additions +]
+- [+ additions +}
+- {- delletions -]
+- [- delletions -}
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index b7810185d168bff4608a01c8c15cada472295d3d..52764f41e0ddd8b7a60b58e1914b317d123daaac 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -93,9 +93,9 @@ describe DiffHelper do
     it "returns strings with marked inline diffs" do
       marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
 
-      expect(marked_old_line).to eq("abc <span class='idiff left right'>&#39;def&#39;</span>")
+      expect(marked_old_line).to eq("abc <span class='idiff left right deletion'>&#39;def&#39;</span>")
       expect(marked_old_line).to be_html_safe
-      expect(marked_new_line).to eq("abc <span class='idiff left right'>&quot;def&quot;</span>")
+      expect(marked_new_line).to eq("abc <span class='idiff left right addition'>&quot;def&quot;</span>")
       expect(marked_new_line).to be_html_safe
     end
   end
diff --git a/spec/lib/banzai/filter/inline_diff_filter_spec.rb b/spec/lib/banzai/filter/inline_diff_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9e526371294667909b1e462b6515297e854b3219
--- /dev/null
+++ b/spec/lib/banzai/filter/inline_diff_filter_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+describe Banzai::Filter::InlineDiffFilter, lib: true do
+  include FilterSpecHelper
+
+  it 'adds inline diff span tags for deletions when using square brackets' do
+    doc = "START [-something deleted-] END"
+    expect(filter(doc).to_html).to eq('START <span class="idiff left right deletion">something deleted</span> END')
+  end
+
+  it 'adds inline diff span tags for deletions when using curley braces' do
+    doc = "START {-something deleted-} END"
+    expect(filter(doc).to_html).to eq('START <span class="idiff left right deletion">something deleted</span> END')
+  end
+
+  it 'does not add inline diff span tags when a closing tag is not provided' do
+    doc = "START [- END"
+    expect(filter(doc).to_html).to eq(doc)
+  end
+
+  it 'adds inline span tags for additions when using square brackets' do
+    doc = "START [+something added+] END"
+    expect(filter(doc).to_html).to eq('START <span class="idiff left right addition">something added</span> END')
+  end
+
+  it 'adds inline span tags for additions  when using curley braces' do
+    doc = "START {+something added+} END"
+    expect(filter(doc).to_html).to eq('START <span class="idiff left right addition">something added</span> END')
+  end
+
+  it 'does not add inline diff span tags when a closing addition tag is not provided' do
+    doc = "START {+ END"
+    expect(filter(doc).to_html).to eq(doc)
+  end
+
+  it 'does not add inline diff span tags when the tags do not match' do
+    examples = [
+      "{+ additions +]",
+      "[+ additions +}",
+      "{- delletions -]",
+      "[- delletions -}"
+    ]
+
+    examples.each do |doc|
+      expect(filter(doc).to_html).to eq(doc)
+    end
+  end
+
+  it 'prevents user-land html being injected' do
+    doc = "START {+&lt;script&gt;alert('I steal cookies')&lt;/script&gt;+} END"
+    expect(filter(doc).to_html).to eq("START <span class=\"idiff left right addition\">&lt;script&gt;alert('I steal cookies')&lt;/script&gt;</span> END")
+  end
+
+  it 'preserves content inside pre tags' do
+    doc = "<pre>START {+something added+} END</pre>"
+    expect(filter(doc).to_html).to eq(doc)
+  end
+
+  it 'preserves content inside code tags' do
+    doc = "<code>START {+something added+} END</code>"
+    expect(filter(doc).to_html).to eq(doc)
+  end
+
+  it 'preserves content inside tt tags' do
+    doc = "<tt>START {+something added+} END</tt>"
+    expect(filter(doc).to_html).to eq(doc)
+  end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 52f693069945eb3c8c818071e902d798f40ceaa0..ff63f2ce8de2f2f500ece2503c9313243f1678d3 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -79,10 +79,10 @@ describe Issues::UpdateService, services: true do
       end
 
       it 'creates system note about title change' do
-        note = find_note('Title changed')
+        note = find_note('Changed title:')
 
         expect(note).not_to be_nil
-        expect(note.note).to eq 'Title changed from **Old title** to **New title**'
+        expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
       end
     end
 
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 213e8c2eb3a17139a45ec7bb48d7682998b281db..56ee3e43056c330381a6b829d5a0dca31f75fe59 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -90,10 +90,10 @@ describe MergeRequests::UpdateService, services: true do
       end
 
       it 'creates system note about title change' do
-        note = find_note('Title changed')
+        note = find_note('Changed title:')
 
         expect(note).not_to be_nil
-        expect(note.note).to eq 'Title changed from **Old title** to **New title**'
+        expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
       end
 
       it 'creates system note about branch change' do
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 5fbf2ae52476d811d6c744b76b82d7711ef21312..3957546b18d8b48f2f2ac6859306ba5f284e258e 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -241,7 +241,7 @@ describe SystemNoteService, services: true do
 
       it 'sets the note text' do
         expect(subject.note).
-          to eq "Title changed from **Old title** to **#{noteable.title}**"
+          to eq "Changed title: **{-Old title-}** → **{+#{noteable.title}+}**"
       end
     end
 
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 43cb6ef43f27f7a17eff412f901c761f052cf08d..259aa469a1869ba4d229e197f9a0536e13e88ffe 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -168,6 +168,16 @@ module MarkdownMatchers
       expect(actual).to have_selector('input[checked]', count: 3)
     end
   end
+
+  # InlineDiffFilter
+  matcher :parse_inline_diffs do
+    set_default_markdown_messages
+
+    match do |actual|
+      expect(actual).to have_selector('span.idiff.addition', count: 2)
+      expect(actual).to have_selector('span.idiff.deletion', count: 2)
+    end
+  end
 end
 
 # Monkeypatch the matcher DSL so that we can reduce some noisy duplication for