diff --git a/CHANGELOG.md b/CHANGELOG.md
index 77b126d854..da480ab676 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -225,6 +225,7 @@
     Bug #5350: An attempt to launch magic bolt causes "AL error invalid value" error
     Bug #5352: Light source items' duration is decremented while they aren't visible
     Feature #1724: Handle AvoidNode
+    Feature #2159: "Graying out" exhausted dialogue topics
     Feature #2229: Improve pathfinding AI
     Feature #3025: Analogue gamepad movement controls
     Feature #3442: Default values for fallbacks from ini file
diff --git a/apps/openmw/mwbase/dialoguemanager.hpp b/apps/openmw/mwbase/dialoguemanager.hpp
index 2ecab1c4cc..e25762f329 100644
--- a/apps/openmw/mwbase/dialoguemanager.hpp
+++ b/apps/openmw/mwbase/dialoguemanager.hpp
@@ -53,6 +53,8 @@ namespace MWBase
 
             virtual bool startDialogue (const MWWorld::Ptr& actor, ResponseCallback* callback) = 0;
 
+            virtual bool inJournal (const std::string& topicId, const std::string& infoId) = 0;
+
             virtual void addTopic (const std::string& topic) = 0;
 
             virtual void addChoice (const std::string& text,int choice) = 0;
@@ -68,7 +70,14 @@ namespace MWBase
             virtual void goodbyeSelected() = 0;
             virtual void questionAnswered (int answer, ResponseCallback* callback) = 0;
 
+            enum TopicType
+            {
+                Specific = 1,
+                Exhausted = 2
+            };
+
             virtual std::list<std::string> getAvailableTopics() = 0;
+            virtual int getTopicFlag(const std::string&) = 0;
 
             virtual bool checkServiceRefused (ResponseCallback* callback) = 0;
 
diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp
index 21bc067d73..3e0cbbad2e 100644
--- a/apps/openmw/mwdialogue/dialoguemanagerimp.cpp
+++ b/apps/openmw/mwdialogue/dialoguemanagerimp.cpp
@@ -228,6 +228,30 @@ namespace MWDialogue
         }
     }
 
+    bool DialogueManager::inJournal (const std::string& topicId, const std::string& infoId)
+    {
+        const MWDialogue::Topic *topicHistory = nullptr;
+        MWBase::Journal *journal = MWBase::Environment::get().getJournal();
+        for (auto it = journal->topicBegin(); it != journal->topicEnd(); ++it)
+        {
+            if (it->first == topicId)
+            {
+                topicHistory = &it->second;
+                break;
+            }
+        }
+
+        if (!topicHistory)
+            return false;
+
+        for(const auto& topic : *topicHistory)
+        {
+            if (topic.mInfoId == infoId)
+                return true;
+        }
+        return false;
+    }
+
     void DialogueManager::executeTopic (const std::string& topic, ResponseCallback* callback)
     {
         Filter filter (mActor, mChoice, mTalkedTo);
@@ -300,22 +324,34 @@ namespace MWDialogue
 
         mActorKnownTopics.clear();
 
-        const MWWorld::Store<ESM::Dialogue> &dialogs =
-            MWBase::Environment::get().getWorld()->getStore().get<ESM::Dialogue>();
+        const auto& dialogs = MWBase::Environment::get().getWorld()->getStore().get<ESM::Dialogue>();
 
         Filter filter (mActor, -1, mTalkedTo);
 
-        for (MWWorld::Store<ESM::Dialogue>::iterator iter = dialogs.begin(); iter != dialogs.end(); ++iter)
+        for (const auto& dialog : dialogs)
         {
-            if (iter->mType == ESM::Dialogue::Topic)
+            if (dialog.mType == ESM::Dialogue::Topic)
             {
-                if (filter.responseAvailable (*iter))
+                const auto* answer = filter.search(dialog, true);
+                auto topicId = Misc::StringUtils::lowerCase(dialog.mId);
+
+                if (answer != nullptr)
                 {
-                    mActorKnownTopics.insert (iter->mId);
+                    int flag = 0;
+                    if(!inJournal(topicId, answer->mId))
+                    {
+                        // Does this dialogue contains some actor-specific answer?
+                        if (answer->mActor == mActor.getCellRef().getRefId())
+                            flag |= MWBase::DialogueManager::TopicType::Specific;
+                    }
+                    else
+                        flag |= MWBase::DialogueManager::TopicType::Exhausted;
+                    mActorKnownTopics.insert (dialog.mId);
+                    mActorKnownTopicsFlag[dialog.mId] = flag;
                 }
+
             }
         }
-
     }
 
     std::list<std::string> DialogueManager::getAvailableTopics()
@@ -336,6 +372,11 @@ namespace MWDialogue
         return keywordList;
     }
 
+    int DialogueManager::getTopicFlag(const std::string& topicId)
+    {
+        return mActorKnownTopicsFlag[topicId];
+    }
+
     void DialogueManager::keywordSelected (const std::string& keyword, ResponseCallback* callback)
     {
         if(!mIsInChoice)
diff --git a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp
index fad0861833..23dd68e918 100644
--- a/apps/openmw/mwdialogue/dialoguemanagerimp.hpp
+++ b/apps/openmw/mwdialogue/dialoguemanagerimp.hpp
@@ -5,6 +5,7 @@
 
 #include <map>
 #include <set>
+#include <unordered_map>
 
 #include <components/compiler/streamerrorhandler.hpp>
 #include <components/translation/translation.hpp>
@@ -30,6 +31,7 @@ namespace MWDialogue
             ModFactionReactionMap mChangedFactionReaction;
 
             std::set<std::string, Misc::StringUtils::CiComp> mActorKnownTopics;
+            std::unordered_map<std::string, int> mActorKnownTopicsFlag;
 
             Translation::Storage& mTranslationDataStorage;
             MWScript::CompilerContext mCompilerContext;
@@ -71,6 +73,9 @@ namespace MWDialogue
             virtual bool startDialogue (const MWWorld::Ptr& actor, ResponseCallback* callback);
 
             std::list<std::string> getAvailableTopics();
+            int getTopicFlag(const std::string& topicId) final;
+
+            bool inJournal (const std::string& topicId, const std::string& infoId) final;
 
             virtual void addTopic (const std::string& topic);
 
diff --git a/apps/openmw/mwdialogue/filter.cpp b/apps/openmw/mwdialogue/filter.cpp
index 042ccb019a..a3c326ab8f 100644
--- a/apps/openmw/mwdialogue/filter.cpp
+++ b/apps/openmw/mwdialogue/filter.cpp
@@ -681,15 +681,3 @@ std::vector<const ESM::DialInfo *> MWDialogue::Filter::list (const ESM::Dialogue
 
     return infos;
 }
-
-bool MWDialogue::Filter::responseAvailable (const ESM::Dialogue& dialogue) const
-{
-    for (ESM::Dialogue::InfoContainer::const_iterator iter = dialogue.mInfo.begin();
-        iter!=dialogue.mInfo.end(); ++iter)
-    {
-        if (testActor (*iter) && testPlayer (*iter) && testSelectStructs (*iter))
-            return true;
-    }
-
-    return false;
-}
diff --git a/apps/openmw/mwdialogue/filter.hpp b/apps/openmw/mwdialogue/filter.hpp
index 4e2ebe6e5a..d2747d59ae 100644
--- a/apps/openmw/mwdialogue/filter.hpp
+++ b/apps/openmw/mwdialogue/filter.hpp
@@ -66,9 +66,6 @@ namespace MWDialogue
             const ESM::DialInfo* search (const ESM::Dialogue& dialogue, const bool fallbackToInfoRefusal) const;
             ///< Get a matching response for the requested dialogue.
             ///  Redirect to "Info Refusal" topic if a response fulfills all conditions but disposition.
-
-            bool responseAvailable (const ESM::Dialogue& dialogue) const;
-            ///< Does a matching response exist? (disposition is ignored for this check)
     };
 }
 
diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp
index bb40bea33f..1dcd8d5ea9 100644
--- a/apps/openmw/mwgui/dialogue.cpp
+++ b/apps/openmw/mwgui/dialogue.cpp
@@ -339,6 +339,7 @@ namespace MWGui
 
         mTopicsList->adjustSize();
         updateHistory();
+        updateTopicFormat();
         mCurrentWindowSize = _sender->getSize();
     }
 
@@ -448,7 +449,6 @@ namespace MWGui
         setTitle(mPtr.getClass().getName(mPtr));
 
         updateTopics();
-        updateTopicsPane(); // force update for new services
 
         updateDisposition();
         restock();
@@ -489,8 +489,6 @@ namespace MWGui
             return;
         mIsCompanion = isCompanion();
         mKeywords = keyWords;
-
-        updateTopicsPane();
     }
 
     void DialogueWindow::updateTopicsPane()
@@ -540,15 +538,16 @@ namespace MWGui
             mTopicsList->addSeparator();
 
 
-        for(std::string& keyword : mKeywords)
+        for(const auto& keyword : mKeywords)
         {
+            std::string topicId = Misc::StringUtils::lowerCase(keyword);
             mTopicsList->addItem(keyword);
 
             Topic* t = new Topic(keyword);
             t->eventTopicActivated += MyGUI::newDelegate(this, &DialogueWindow::onTopicActivated);
-            mTopicLinks[Misc::StringUtils::lowerCase(keyword)] = t;
+            mTopicLinks[topicId] = t;
 
-            mKeywordSearch.seed(Misc::StringUtils::lowerCase(keyword), intptr_t(t));
+            mKeywordSearch.seed(topicId, intptr_t(t));
         }
         mTopicsList->adjustSize();
 
@@ -734,9 +733,28 @@ namespace MWGui
             updateHistory();
     }
 
+    void DialogueWindow::updateTopicFormat()
+    {
+        std::string specialColour = Settings::Manager::getString("color topic specific", "GUI");
+        std::string oldColour = Settings::Manager::getString("color topic exhausted", "GUI");
+
+        for (const std::string& keyword : mKeywords)
+        {
+            int flag = MWBase::Environment::get().getDialogueManager()->getTopicFlag(keyword);
+            MyGUI::Button* button = mTopicsList->getItemWidget(keyword);
+
+            if (!specialColour.empty() && flag & MWBase::DialogueManager::TopicType::Specific)
+                button->getSubWidgetText()->setTextColour(MyGUI::Colour::parse(specialColour));
+            else if (!oldColour.empty() && flag & MWBase::DialogueManager::TopicType::Exhausted)
+                button->getSubWidgetText()->setTextColour(MyGUI::Colour::parse(oldColour));
+        }
+    }
+
     void DialogueWindow::updateTopics()
     {
         setKeywords(MWBase::Environment::get().getDialogueManager()->getAvailableTopics());
+        updateTopicsPane();
+        updateTopicFormat();
     }
 
     bool DialogueWindow::isCompanion()
diff --git a/apps/openmw/mwgui/dialogue.hpp b/apps/openmw/mwgui/dialogue.hpp
index 2c3fb1a44c..d9c26ef203 100644
--- a/apps/openmw/mwgui/dialogue.hpp
+++ b/apps/openmw/mwgui/dialogue.hpp
@@ -186,6 +186,8 @@ namespace MWGui
 
         std::unique_ptr<ResponseCallback> mCallback;
         std::unique_ptr<ResponseCallback> mGreetingCallback;
+
+        void updateTopicFormat();
     };
 }
 #endif
diff --git a/docs/source/reference/modding/settings/GUI.rst b/docs/source/reference/modding/settings/GUI.rst
index c8f4e16f80..b881221b33 100644
--- a/docs/source/reference/modding/settings/GUI.rst
+++ b/docs/source/reference/modding/settings/GUI.rst
@@ -140,3 +140,29 @@ The alpha value is currently ignored.
 This setting can only be configured by editing the settings configuration file.
 This setting has no effect if the crosshair setting in the HUD Settings Section is false.
 This setting has no effect if the show owned setting in the Game Settings Section is false.
+
+color topic specific
+--------------------
+
+:Type:		RGBA floating point
+:Range:		0.0 to 1.0
+:Default:	empty
+
+This setting overrides the color of keywords in the dialogue topic window.
+The value is composed of four floating point values representing the red, green, blue and alpha channels.
+The alpha value is currently ignored.
+
+The color is overriden if the actor is about to give an answer that is unique to him (that is, dialogue with their object ID in the Actor field) that wasn't seen yet.
+
+color topic exhausted
+---------------------
+
+:Type:		RGBA floating point
+:Range:		0.0 to 1.0
+:Default:	empty
+
+This setting overrides the color of keywords in the dialogue topic window.
+The value is composed of four floating point values representing the red, green, blue and alpha channels.
+The alpha value is currently ignored.
+
+The color is overridden if the next actor responses to the topic keyword has already been seen by the player.
diff --git a/files/settings-default.cfg b/files/settings-default.cfg
index 6703e77326..3203d6743f 100644
--- a/files/settings-default.cfg
+++ b/files/settings-default.cfg
@@ -190,6 +190,14 @@ color crosshair owned = 1.0 0.15 0.15 1.0
 # Controls whether Arrow keys, Movement keys, Tab/Shift-Tab and Spacebar/Enter/Activate may be used to navigate GUI buttons.
 keyboard navigation = true
 
+# The color of dialogue topic keywords that gives unique actor responses
+# Format R G B A or empty for default
+color topic specific =
+
+# The color of dialogue topic keywords that gives already read responses
+# Format R G B A or empty for default
+color topic exhausted =
+
 [HUD]
 
 # Displays the crosshair or reticle when not in GUI mode.