diff --git a/app/assets/javascripts/discourse/app/components/sidebar/tags-section.js b/app/assets/javascripts/discourse/app/components/sidebar/tags-section.js
index c763d22bfe7..d5f9ba748fb 100644
--- a/app/assets/javascripts/discourse/app/components/sidebar/tags-section.js
+++ b/app/assets/javascripts/discourse/app/components/sidebar/tags-section.js
@@ -4,10 +4,27 @@ import GlimmerComponent from "discourse/components/glimmer";
 import TagSectionLink from "discourse/lib/sidebar/tags-section/tag-section-link";
 
 export default class SidebarTagsSection extends GlimmerComponent {
+  constructor() {
+    super(...arguments);
+
+    this.callbackId = this.topicTrackingState.onStateChange(() => {
+      this.sectionLinks.forEach((sectionLink) => {
+        sectionLink.refreshCounts();
+      });
+    });
+  }
+
+  willDestroy() {
+    this.topicTrackingState.offStateChange(this.callbackId);
+  }
+
   @cached
   get sectionLinks() {
     return this.currentUser.trackedTags.map((trackedTag) => {
-      return new TagSectionLink({ tag: trackedTag });
+      return new TagSectionLink({
+        tag: trackedTag,
+        topicTrackingState: this.topicTrackingState,
+      });
     });
   }
 }
diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/tags-section/tag-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/tags-section/tag-section-link.js
index ff78f2db4e0..cf161a1aff7 100644
--- a/app/assets/javascripts/discourse/app/lib/sidebar/tags-section/tag-section-link.js
+++ b/app/assets/javascripts/discourse/app/lib/sidebar/tags-section/tag-section-link.js
@@ -1,6 +1,30 @@
+import I18n from "I18n";
+
+import { tracked } from "@glimmer/tracking";
+
+import { bind } from "discourse-common/utils/decorators";
+
 export default class TagSectionLink {
-  constructor({ tag }) {
+  @tracked totalUnread = 0;
+  @tracked totalNew = 0;
+
+  constructor({ tag, topicTrackingState }) {
     this.tag = tag;
+    this.topicTrackingState = topicTrackingState;
+    this.refreshCounts();
+  }
+
+  @bind
+  refreshCounts() {
+    this.totalUnread = this.topicTrackingState.countUnread({
+      tagId: this.tag,
+    });
+
+    if (this.totalUnread === 0) {
+      this.totalNew = this.topicTrackingState.countNew({
+        tagId: this.tag,
+      });
+    }
   }
 
   get name() {
@@ -22,4 +46,26 @@ export default class TagSectionLink {
   get text() {
     return this.tag;
   }
+
+  get badgeText() {
+    if (this.totalUnread > 0) {
+      return I18n.t("sidebar.unread_count", {
+        count: this.totalUnread,
+      });
+    } else if (this.totalNew > 0) {
+      return I18n.t("sidebar.new_count", {
+        count: this.totalNew,
+      });
+    }
+  }
+
+  get route() {
+    if (this.totalUnread > 0) {
+      return "tag.showUnread";
+    } else if (this.totalNew > 0) {
+      return "tag.showNew";
+    } else {
+      return "tag.show";
+    }
+  }
 }
diff --git a/app/assets/javascripts/discourse/app/templates/components/sidebar/tags-section.hbs b/app/assets/javascripts/discourse/app/templates/components/sidebar/tags-section.hbs
index 60c9b0fbbb3..78194b4de41 100644
--- a/app/assets/javascripts/discourse/app/templates/components/sidebar/tags-section.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/sidebar/tags-section.hbs
@@ -12,6 +12,7 @@
         @title={{sectionLink.title}}
         @content={{sectionLink.text}}
         @currentWhen={{sectionLink.currentWhen}}
+        @badgeText={{sectionLink.badgeText}}
         @model={{sectionLink.model}}>
       </Sidebar::SectionLink>
     {{/each}}
diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-tags-section-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-tags-section-test.js
index bed88ce3d04..03c26f09922 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-tags-section-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-tags-section-test.js
@@ -1,11 +1,12 @@
 import I18n from "I18n";
 
-import { click, currentURL, visit } from "@ember/test-helpers";
+import { click, currentURL, settled, visit } from "@ember/test-helpers";
 
 import {
   acceptance,
   conditionalTest,
   exists,
+  publishToMessageBus,
   query,
   queryAll,
   updateCurrentUser,
@@ -226,4 +227,147 @@ acceptance("Sidebar - Tags section", function (needs) {
       );
     }
   );
+
+  conditionalTest(
+    "new and unread count for tag section links",
+    !isLegacyEmber(),
+    async function (assert) {
+      this.container.lookup("topic-tracking-state:main").loadStates([
+        {
+          topic_id: 1,
+          highest_post_number: 1,
+          last_read_post_number: null,
+          created_at: "2022-05-11T03:09:31.959Z",
+          category_id: 1,
+          notification_level: null,
+          created_in_new_period: true,
+          unread_not_too_old: true,
+          treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z",
+          tags: ["tag1"],
+        },
+        {
+          topic_id: 2,
+          highest_post_number: 12,
+          last_read_post_number: 11,
+          created_at: "2020-02-09T09:40:02.672Z",
+          category_id: 2,
+          notification_level: 2,
+          created_in_new_period: false,
+          unread_not_too_old: true,
+          treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z",
+          tags: ["tag1"],
+        },
+        {
+          topic_id: 3,
+          highest_post_number: 15,
+          last_read_post_number: 14,
+          created_at: "2021-06-14T12:41:02.477Z",
+          category_id: 3,
+          notification_level: 2,
+          created_in_new_period: false,
+          unread_not_too_old: true,
+          treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z",
+          tags: ["tag2"],
+        },
+        {
+          topic_id: 4,
+          highest_post_number: 17,
+          last_read_post_number: 16,
+          created_at: "2020-10-31T03:41:42.257Z",
+          category_id: 4,
+          notification_level: 2,
+          created_in_new_period: false,
+          unread_not_too_old: true,
+          treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z",
+          tags: ["tag4"],
+        },
+      ]);
+
+      await visit("/");
+
+      assert.strictEqual(
+        query(
+          `.sidebar-section-link-tag1 .sidebar-section-link-content-badge`
+        ).textContent.trim(),
+        I18n.t("sidebar.unread_count", { count: 1 }),
+        `displays 1 unread count for tag1 section link`
+      );
+
+      assert.strictEqual(
+        query(
+          `.sidebar-section-link-tag2 .sidebar-section-link-content-badge`
+        ).textContent.trim(),
+        I18n.t("sidebar.unread_count", { count: 1 }),
+        `displays 1 unread count for tag2 section link`
+      );
+
+      assert.ok(
+        !exists(
+          `.sidebar-section-link-tag3 .sidebar-section-link-content-badge`
+        ),
+        "does not display any badge for tag3 section link"
+      );
+
+      publishToMessageBus("/unread", {
+        topic_id: 2,
+        message_type: "read",
+        payload: {
+          last_read_post_number: 12,
+          highest_post_number: 12,
+        },
+      });
+
+      await settled();
+
+      assert.strictEqual(
+        query(
+          `.sidebar-section-link-tag1 .sidebar-section-link-content-badge`
+        ).textContent.trim(),
+        I18n.t("sidebar.new_count", { count: 1 }),
+        `displays 1 new count for tag1 section link`
+      );
+
+      publishToMessageBus("/unread", {
+        topic_id: 1,
+        message_type: "read",
+        payload: {
+          last_read_post_number: 1,
+          highest_post_number: 1,
+        },
+      });
+
+      await settled();
+
+      assert.ok(
+        !exists(
+          `.sidebar-section-link-tag1 .sidebar-section-link-content-badge`
+        ),
+        `does not display any badge tag1 section link`
+      );
+    }
+  );
+
+  conditionalTest(
+    "cleans up topic tracking state state changed callbacks when section is destroyed",
+    !isLegacyEmber(),
+    async function (assert) {
+      await visit("/");
+
+      const topicTrackingState = this.container.lookup(
+        "topic-tracking-state:main"
+      );
+
+      const initialCallbackCount = Object.keys(
+        topicTrackingState.stateChangeCallbacks
+      ).length;
+
+      await click(".header-sidebar-toggle .btn");
+      await click(".header-sidebar-toggle .btn");
+
+      assert.strictEqual(
+        Object.keys(topicTrackingState.stateChangeCallbacks).length,
+        initialCallbackCount
+      );
+    }
+  );
 });
diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb
index 1b143b5c2b4..eeec84c2e87 100644
--- a/app/models/topic_tracking_state.rb
+++ b/app/models/topic_tracking_state.rb
@@ -278,7 +278,7 @@ class TopicTrackingState
   end
 
   def self.include_tags_in_report?
-    SiteSetting.tagging_enabled && @include_tags_in_report
+    SiteSetting.tagging_enabled && (@include_tags_in_report || SiteSetting.enable_experimental_sidebar)
   end
 
   def self.include_tags_in_report=(v)
diff --git a/app/serializers/topic_tracking_state_serializer.rb b/app/serializers/topic_tracking_state_serializer.rb
index b26bd2995c2..473997204f2 100644
--- a/app/serializers/topic_tracking_state_serializer.rb
+++ b/app/serializers/topic_tracking_state_serializer.rb
@@ -9,7 +9,8 @@ class TopicTrackingStateSerializer < ApplicationSerializer
              :notification_level,
              :created_in_new_period,
              :unread_not_too_old,
-             :treat_as_new_topic_start_date
+             :treat_as_new_topic_start_date,
+             :tags
 
   def created_in_new_period
     return true if !scope
@@ -20,4 +21,8 @@ class TopicTrackingStateSerializer < ApplicationSerializer
     return true if object.first_unread_at.blank?
     object.updated_at >= object.first_unread_at
   end
+
+  def include_tags?
+    object.respond_to?(:tags)
+  end
 end
diff --git a/spec/models/topic_tracking_state_spec.rb b/spec/models/topic_tracking_state_spec.rb
index e66a8d5f56e..17f13a836a6 100644
--- a/spec/models/topic_tracking_state_spec.rb
+++ b/spec/models/topic_tracking_state_spec.rb
@@ -539,14 +539,7 @@ describe TopicTrackingState do
   end
 
   context "tag support" do
-    after do
-      # this is a bit of an odd hook, but this is a global change
-      # used by plugins that leverage tagging heavily and need
-      # tag information in topic tracking state
-      TopicTrackingState.include_tags_in_report = false
-    end
-
-    it "correctly handles tags" do
+    before do
       SiteSetting.tagging_enabled = true
 
       post.topic.notifier.watch_topic!(post.topic.user_id)
@@ -556,6 +549,27 @@ describe TopicTrackingState do
         Guardian.new(Discourse.system_user),
         ['bananas', 'apples']
       )
+    end
+
+    it "includes tags when SiteSetting.enable_experimental_sidebar is true" do
+      report = TopicTrackingState.report(user)
+      expect(report.length).to eq(1)
+      row = report[0]
+      expect(row.respond_to?(:tags)).to eq(false)
+
+      SiteSetting.enable_experimental_sidebar = true
+
+      report = TopicTrackingState.report(user)
+      expect(report.length).to eq(1)
+      row = report[0]
+      expect(row.tags).to contain_exactly("apples", "bananas")
+    end
+
+    it "includes tags when TopicTrackingState.include_tags_in_report option is enabled" do
+      report = TopicTrackingState.report(user)
+      expect(report.length).to eq(1)
+      row = report[0]
+      expect(row.respond_to? :tags).to eq(false)
 
       TopicTrackingState.include_tags_in_report = true
 
@@ -563,13 +577,8 @@ describe TopicTrackingState do
       expect(report.length).to eq(1)
       row = report[0]
       expect(row.tags).to contain_exactly("apples", "bananas")
-
+    ensure
       TopicTrackingState.include_tags_in_report = false
-
-      report = TopicTrackingState.report(user)
-      expect(report.length).to eq(1)
-      row = report[0]
-      expect(row.respond_to? :tags).to eq(false)
     end
   end
 
diff --git a/spec/serializers/topic_tracking_state_serializer_spec.rb b/spec/serializers/topic_tracking_state_serializer_spec.rb
new file mode 100644
index 00000000000..c826daad109
--- /dev/null
+++ b/spec/serializers/topic_tracking_state_serializer_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+describe TopicTrackingStateSerializer do
+  fab!(:user) { Fabricate(:user) }
+  fab!(:post) { create_post }
+
+  it 'serializes topic tracking state reports' do
+    report = TopicTrackingState.report(user)
+    serialized = described_class.new(report[0], scope: Guardian.new(user), root: false).as_json
+
+    expect(serialized[:topic_id]).to eq(post.topic_id)
+    expect(serialized[:highest_post_number]).to eq(post.topic.highest_post_number)
+    expect(serialized[:last_read_post_number]).to eq(nil)
+    expect(serialized[:created_at]).to be_present
+    expect(serialized[:notification_level]).to eq(nil)
+    expect(serialized[:created_in_new_period]).to eq(true)
+    expect(serialized[:unread_not_too_old]).to eq(true)
+    expect(serialized[:treat_as_new_topic_start_date]).to be_present
+    expect(serialized.has_key?(:tags)).to eq(false)
+  end
+
+  it "includes tags attribute when tags are present" do
+    TopicTrackingState.include_tags_in_report = true
+
+    post.topic.notifier.watch_topic!(post.topic.user_id)
+
+    DiscourseTagging.tag_topic_by_names(
+      post.topic,
+      Guardian.new(Discourse.system_user),
+      ['bananas', 'apples']
+    )
+
+    report = TopicTrackingState.report(user)
+    serialized = described_class.new(report[0], scope: Guardian.new(user), root: false).as_json
+
+    expect(serialized[:tags]).to contain_exactly("bananas", "apples")
+  ensure
+    TopicTrackingState.include_tags_in_report = false
+  end
+end