diff --git a/src/contents/ui/room-settings/RoomSettingsPage.qml b/src/contents/ui/room-settings/RoomSettingsPage.qml
--- a/src/contents/ui/room-settings/RoomSettingsPage.qml
+++ b/src/contents/ui/room-settings/RoomSettingsPage.qml
@@ -26,6 +26,26 @@
   }
   property var roomDisplayName: roomNameProvider.name
   property var customTagIds: room.tagIds.filter(k => k.startsWith('u.'))
+  property var editingTopic: false
+  property var submittingTopic: false
+  property var submitTopic: Kazv.AsyncHandler {
+    trigger: () => {
+      roomSettingsPage.submittingTopic = true;
+      return roomSettingsPage.room.setTopic(roomTopicEdit.text);
+    }
+    onResolved: (success, data) => {
+      roomSettingsPage.submittingTopic = false;
+      if (success) {
+        roomSettingsPage.editingTopic = false;
+      } else {
+        showPassiveNotification(l10n.get('room-settings-set-topic-failed-prompt', {
+          room: room.roomId,
+          errorCode: data.errorCode,
+          errorMsg: data.error,
+        }));
+      }
+    }
+  }
 
   title: l10n.get('room-settings-page-title', { room: roomDisplayName })
 
@@ -79,7 +99,7 @@
   property var members: room.members()
 
   property var avatarCount: 4
-  
+
   ColumnLayout {
     RowLayout {
       Layout.alignment: Qt.AlignHCenter
@@ -125,6 +145,49 @@
         visible: !!room.name
       }
     }
+    RowLayout {
+      visible: !roomSettingsPage.editingTopic
+      Kazv.SelectableText {
+        objectName: 'roomTopicLabel'
+        text: room.topic || l10n.get('room-settings-topic-missing')
+        font.italic: !room.topic
+        wrapMode: Text.Wrap
+        Kirigami.Theme.colorGroup: !!room.topic ? Kirigami.Theme.Normal : Kirigami.Theme.Inactive
+        Layout.fillWidth: true
+      }
+      Button {
+        Layout.alignment: Qt.AlignTop
+        objectName: 'editTopicButton'
+        text: l10n.get('room-settings-edit-topic-action')
+        onClicked: {
+          roomTopicEdit.text = room.topic;
+          roomSettingsPage.editingTopic = true;
+        }
+      }
+    }
+    RowLayout {
+      visible: roomSettingsPage.editingTopic
+      TextArea {
+        id: roomTopicEdit
+        objectName: 'roomTopicEdit'
+        Layout.fillWidth: true
+        wrapMode: Text.Wrap
+      }
+      Button {
+        Layout.alignment: Qt.AlignTop
+        objectName: 'saveTopicButton'
+        enabled: !roomSettingsPage.submittingTopic
+        text: l10n.get('room-settings-save-topic-action')
+        onClicked: roomSettingsPage.submitTopic.call()
+      }
+      Button {
+        Layout.alignment: Qt.AlignTop
+        objectName: 'discardTopicButton'
+        enabled: !roomSettingsPage.submittingTopic
+        text: l10n.get('room-settings-discard-topic-action')
+        onClicked: roomSettingsPage.editingTopic = false
+      }
+    }
     Button {
       text: l10n.get('room-settings-members-action')
       icon.name: 'im-user'
diff --git a/src/l10n/cmn-Hans/100-ui.ftl b/src/l10n/cmn-Hans/100-ui.ftl
--- a/src/l10n/cmn-Hans/100-ui.ftl
+++ b/src/l10n/cmn-Hans/100-ui.ftl
@@ -93,6 +93,11 @@
 room-settings-not-encrypted = 本房间中的消息没有端对端加密。
 room-settings-encryption-enabled-notification = 本房间中的加密已经启用。
 room-settings-encryption-failed-to-enable-notification = 不能在本房间中启用加密。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
+room-settings-topic-missing = 这个房间没有话题。
+room-settings-edit-topic-action = 编辑话题
+room-settings-save-topic-action = 保存话题
+room-settings-discard-topic-action = 放弃话题
+room-settings-set-topic-failed-prompt = 不能设置话题。错误代码:{ $errorCode }。错误讯息:{ $errorMsg }。
 
 room-member-list-page-title = { $room } 的成员
 
diff --git a/src/l10n/en/100-ui.ftl b/src/l10n/en/100-ui.ftl
--- a/src/l10n/en/100-ui.ftl
+++ b/src/l10n/en/100-ui.ftl
@@ -97,6 +97,11 @@
 room-settings-not-encrypted = Messages in this room are not end-to-end-encrypted.
 room-settings-encryption-enabled-notification = Encryption is now enabled in this room.
 room-settings-encryption-failed-to-enable-notification = Cannot enable encryption in this room. Error code: { $errorCode }. Error message: { $errorMsg }.
+room-settings-topic-missing = This room does not have a topic.
+room-settings-edit-topic-action = Edit topic
+room-settings-save-topic-action = Save topic
+room-settings-discard-topic-action = Discard topic
+room-settings-set-topic-failed-prompt = Cannot set topic. Error code: { $errorCode }. Error message: { $errorMsg }.
 
 room-member-list-page-title = Members of { $room }
 
diff --git a/src/matrix-room.hpp b/src/matrix-room.hpp
--- a/src/matrix-room.hpp
+++ b/src/matrix-room.hpp
@@ -49,6 +49,7 @@
 
     LAGER_QT_READER(QString, roomId);
     LAGER_QT_READER(QString, name);
+    LAGER_QT_READER(QString, topic);
     LAGER_QT_READER(QStringList, heroNames);
     LAGER_QT_READER(QVariant, heroEvents);
     LAGER_QT_READER(QString, avatarMxcUri);
@@ -134,6 +135,8 @@
 
     Q_INVOKABLE MatrixPromise *postReadReceipt(const QString &eventId) const;
 
+    Q_INVOKABLE MatrixPromise *setTopic(const QString &newTopic) const;
+
 Q_SIGNALS:
     void powerLevelsChanged();
 
diff --git a/src/matrix-room.cpp b/src/matrix-room.cpp
--- a/src/matrix-room.cpp
+++ b/src/matrix-room.cpp
@@ -41,6 +41,7 @@
     , m_powerLevels(m_room.powerLevels())
     , LAGER_QT(roomId)(m_room.roomId().xform(strToQt))
     , LAGER_QT(name)(m_room.nameOpt()[lager::lenses::or_default].xform(strToQt))
+    , LAGER_QT(topic)(m_room.topic().xform(strToQt))
     , LAGER_QT(heroNames)(m_room.heroDisplayNames().xform(strListToQt))
     , LAGER_QT(heroEvents)(m_room.heroMemberEvents()
         .map([](const auto &events) {
@@ -499,3 +500,8 @@
 {
     return new MatrixPromise(m_room.postReceipt(eventId.toStdString()));
 }
+
+MatrixPromise *MatrixRoom::setTopic(const QString &newTopic) const
+{
+    return new MatrixPromise(m_room.setTopic(newTopic.toStdString()));
+}
diff --git a/src/tests/quick-tests/tst_RoomSettingsPage.qml b/src/tests/quick-tests/tst_RoomSettingsPage.qml
--- a/src/tests/quick-tests/tst_RoomSettingsPage.qml
+++ b/src/tests/quick-tests/tst_RoomSettingsPage.qml
@@ -41,6 +41,7 @@
   property var roomJoined: Helpers.factory.room({
     membership: MK.MatrixRoom.Join,
     leaveRoom: mockHelper.promise(),
+    topic: '',
   })
 
   property var roomWithMembers: Helpers.factory.room({
@@ -62,6 +63,12 @@
     roomId: '!someid:example.com',
   })
 
+  property var roomWithTopic: Helpers.factory.room({
+    membership: MK.MatrixRoom.Join,
+    topic: 'some topic',
+    setTopic: mockHelper.promise(),
+  })
+
   property var showPassiveNotification: mockHelper.noop()
 
   property var l10n: Helpers.fluentMock
@@ -100,6 +107,11 @@
       id: pageWithoutName
       room: item.roomWithoutName
     }
+
+    KazvRS.RoomSettingsPage {
+      id: pageWithTopic
+      room: item.roomWithTopic
+    }
   }
 
   TestCase {
@@ -107,7 +119,16 @@
     name: 'RoomSettingsPageTest'
     when: windowShown
 
+    function initTestCase() {
+      pageJoined.contentItem.clip = false;
+      pageWithTopic.contentItem.clip = false;
+    }
+
     function init() {
+      pageJoined.submittingTopic = false;
+      pageJoined.editingTopic = false;
+      pageWithTopic.submittingTopic = false;
+      pageWithTopic.editingTopic = false;
       mockHelper.clearAll();
     }
 
@@ -188,6 +209,80 @@
       verify(nameLabel.text === '!someid:example.com');
       verify(!idLabel.visible)
     }
-    
+
+    function test_roomWithoutTopic() {
+      const topicLabel = findChild(pageJoined, 'roomTopicLabel');
+      verify(topicLabel.font.italic);
+      compare(topicLabel.text, l10n.get('room-settings-topic-missing'));
+    }
+
+    function test_roomWithTopic() {
+      const topicLabel = findChild(pageWithTopic, 'roomTopicLabel');
+      verify(!topicLabel.font.italic);
+      compare(topicLabel.text, 'some topic');
+    }
+
+    function test_editRoomTopic() {
+      const editButton = findChild(pageWithTopic, 'editTopicButton');
+      mouseClick(editButton);
+      const textArea = findChild(pageWithTopic, 'roomTopicEdit');
+      tryVerify(() => textArea.visible);
+      verify(textArea.text === 'some topic');
+      textArea.text = 'other topic';
+      const saveTopicButton = findChild(pageWithTopic, 'saveTopicButton');
+      verify(saveTopicButton.visible);
+      verify(saveTopicButton.enabled);
+      const discardTopicButton = findChild(pageWithTopic, 'discardTopicButton');
+      verify(discardTopicButton.visible);
+      verify(discardTopicButton.enabled);
+      mouseClick(saveTopicButton);
+      tryVerify(() => roomWithTopic.setTopic.calledTimes() === 1);
+      compare(roomWithTopic.setTopic.lastArgs()[0], 'other topic');
+      verify(!saveTopicButton.enabled);
+      verify(!discardTopicButton.enabled);
+
+      roomWithTopic.setTopic.lastRetVal().resolve(true, {});
+      tryVerify(() => editButton.visible);
+      verify(!saveTopicButton.visible);
+    }
+
+    function test_editRoomTopicFailed() {
+      const editButton = findChild(pageWithTopic, 'editTopicButton');
+      mouseClick(editButton);
+      const textArea = findChild(pageWithTopic, 'roomTopicEdit');
+      tryVerify(() => textArea.visible);
+      verify(textArea.text === 'some topic');
+      textArea.text = 'other topic';
+      const saveTopicButton = findChild(pageWithTopic, 'saveTopicButton');
+      mouseClick(saveTopicButton);
+      tryVerify(() => roomWithTopic.setTopic.calledTimes() === 1);
+      roomWithTopic.setTopic.lastRetVal().resolve(false, {});
+      verify(saveTopicButton.enabled);
+      verify(textArea.visible);
+    }
+
+    function test_editRoomTopicDiscard() {
+      const editButton = findChild(pageWithTopic, 'editTopicButton');
+      mouseClick(editButton);
+      const textArea = findChild(pageWithTopic, 'roomTopicEdit');
+      tryVerify(() => textArea.visible);
+      verify(textArea.text === 'some topic');
+      textArea.text = 'other topic';
+      const discardTopicButton = findChild(pageWithTopic, 'discardTopicButton');
+      mouseClick(discardTopicButton);
+      compare(roomWithTopic.setTopic.calledTimes(), 0);
+      verify(!textArea.visible);
+      mouseClick(editButton);
+      tryVerify(() => textArea.visible);
+      verify(textArea.text === 'some topic');
+    }
+
+    function test_addRoomTopic() {
+      const editButton = findChild(pageJoined, 'editTopicButton');
+      mouseClick(editButton);
+      const textArea = findChild(pageJoined, 'roomTopicEdit');
+      tryVerify(() => textArea.visible);
+      verify(textArea.text === '');
+    }
   }
 }