diff --git a/tree/treeplayer/inc/ROOT/TTreeProcessorMT.hxx b/tree/treeplayer/inc/ROOT/TTreeProcessorMT.hxx
index 15d05068b89acdae6f87c870d5a70142975c93e7..b3fdc5fcf34e1d2cae344f6e342dd500dbb3bc04 100644
--- a/tree/treeplayer/inc/ROOT/TTreeProcessorMT.hxx
+++ b/tree/treeplayer/inc/ROOT/TTreeProcessorMT.hxx
@@ -170,7 +170,7 @@ namespace ROOT {
       const TEntryList fEntryList; // const to be sure to avoid race conditions among TTreeViews
       const Internal::FriendInfo fFriendInfo;
 
-      ROOT::TThreadedObject<ROOT::Internal::TTreeView> treeView; ///<! Thread-local TreeViews
+      ROOT::TThreadedObject<ROOT::Internal::TTreeView> fTreeView; ///<! Thread-local TreeViews
 
       Internal::FriendInfo GetFriendInfo(TTree &tree);
       std::string FindTreeName();
diff --git a/tree/treeplayer/src/TTreeProcessorMT.cxx b/tree/treeplayer/src/TTreeProcessorMT.cxx
index 025565dc37b28b3fd8daf4823bcd991a4d190146..27493a0042439807484fbb32faad1d80a004cceb 100644
--- a/tree/treeplayer/src/TTreeProcessorMT.cxx
+++ b/tree/treeplayer/src/TTreeProcessorMT.cxx
@@ -35,6 +35,119 @@ namespace ROOT {
 unsigned int TTreeProcessorMT::fgMaxTasksPerFilePerWorker = 24U;
 
 namespace Internal {
+
+/// A cluster of entries
+struct EntryCluster {
+   Long64_t start;
+   Long64_t end;
+};
+
+/// Names, aliases, and file names of a TTree's or TChain's friends
+using NameAlias = std::pair<std::string, std::string>;
+struct FriendInfo {
+   /// Pairs of names and aliases of friend trees/chains
+   std::vector<Internal::NameAlias> fFriendNames;
+   /// Names of the files where each friend is stored. fFriendFileNames[i] is the list of files for friend with
+   /// name fFriendNames[i]
+   std::vector<std::vector<std::string>> fFriendFileNames;
+};
+
+class TTreeView {
+private:
+   using TreeReaderEntryListPair = std::pair<std::unique_ptr<TTreeReader>, std::unique_ptr<TEntryList>>;
+
+   // NOTE: fFriends must come before fChain to be deleted after it, see ROOT-9281 for more details
+   std::vector<std::unique_ptr<TChain>> fFriends; ///< Friends of the tree/chain
+   std::unique_ptr<TChain> fChain;                ///< Chain on which to operate
+
+   ////////////////////////////////////////////////////////////////////////////////
+   /// Construct fChain, also adding friends if needed and injecting knowledge of offsets if available.
+   void MakeChain(const std::string &treeName, const std::vector<std::string> &fileNames, const FriendInfo &friendInfo,
+                  const std::vector<Long64_t> &nEntries, const std::vector<std::vector<Long64_t>> &friendEntries)
+   {
+      const std::vector<NameAlias> &friendNames = friendInfo.fFriendNames;
+      const std::vector<std::vector<std::string>> &friendFileNames = friendInfo.fFriendFileNames;
+
+      fChain.reset(new TChain(treeName.c_str()));
+      const auto nFiles = fileNames.size();
+      for (auto i = 0u; i < nFiles; ++i) {
+         fChain->Add(fileNames[i].c_str(), nEntries[i]);
+      }
+      fChain->ResetBit(TObject::kMustCleanup);
+
+      fFriends.clear();
+      const auto nFriends = friendNames.size();
+      for (auto i = 0u; i < nFriends; ++i) {
+         const auto &friendName = friendNames[i];
+         const auto &name = friendName.first;
+         const auto &alias = friendName.second;
+
+         // Build a friend chain
+         auto frChain = std::make_unique<TChain>(name.c_str());
+         const auto nFileNames = friendFileNames[i].size();
+         for (auto j = 0u; j < nFileNames; ++j)
+            frChain->Add(friendFileNames[i][j].c_str(), friendEntries[i][j]);
+
+         // Make it friends with the main chain
+         fChain->AddFriend(frChain.get(), alias.c_str());
+         fFriends.emplace_back(std::move(frChain));
+      }
+   }
+
+   TreeReaderEntryListPair MakeReaderWithEntryList(TEntryList &globalList, Long64_t start, Long64_t end)
+   {
+      // TEntryList and SetEntriesRange do not work together (the former has precedence).
+      // We need to construct a TEntryList that contains only those entry numbers in our desired range.
+      auto localList = std::make_unique<TEntryList>();
+      Long64_t entry = globalList.GetEntry(0);
+      do {
+         if (entry >= end)
+            break;
+         else if (entry >= start)
+            localList->Enter(entry);
+      } while ((entry = globalList.Next()) >= 0);
+
+      auto reader = std::make_unique<TTreeReader>(fChain.get(), localList.get());
+      return std::make_pair(std::move(reader), std::move(localList));
+   }
+
+   std::unique_ptr<TTreeReader> MakeReader(Long64_t start, Long64_t end)
+   {
+      auto reader = std::make_unique<TTreeReader>(fChain.get());
+      reader->SetEntriesRange(start, end);
+      return reader;
+   }
+
+public:
+   TTreeView() {}
+
+   // no-op, we don't want to copy the local TChains
+   TTreeView(const TTreeView &) {}
+
+   //////////////////////////////////////////////////////////////////////////
+   /// Get a TTreeReader for the current tree of this view.
+   TreeReaderEntryListPair GetTreeReader(Long64_t start, Long64_t end, const std::string &treeName,
+                                         const std::vector<std::string> &fileNames, const FriendInfo &friendInfo,
+                                         TEntryList entryList, const std::vector<Long64_t> &nEntries,
+                                         const std::vector<std::vector<Long64_t>> &friendEntries)
+   {
+      const bool usingLocalEntries = friendInfo.fFriendNames.empty() && entryList.GetN() == 0;
+      if (fChain == nullptr || (usingLocalEntries && fileNames[0] != fChain->GetListOfFiles()->At(0)->GetTitle()))
+         MakeChain(treeName, fileNames, friendInfo, nEntries, friendEntries);
+
+      std::unique_ptr<TTreeReader> reader;
+      std::unique_ptr<TEntryList> localList;
+      if (entryList.GetN() > 0) {
+         std::tie(reader, localList) = MakeReaderWithEntryList(entryList, start, end);
+      } else {
+         reader = MakeReader(start, end);
+      }
+
+      // we need to return the entry list too, as it needs to be in scope as long as the reader is
+      return std::make_pair(std::move(reader), std::move(localList));
+   }
+};
+
 ////////////////////////////////////////////////////////////////////////
 /// Return a vector of cluster boundaries for the given tree and files.
 // EntryClusters and number of entries per file
@@ -388,8 +501,8 @@ void TTreeProcessorMT::Process(std::function<void(TTreeReader &)> func)
       auto processCluster = [&](const Internal::EntryCluster &c) {
          std::unique_ptr<TTreeReader> reader;
          std::unique_ptr<TEntryList> elist;
-         std::tie(reader, elist) = treeView->GetTreeReader(c.start, c.end, fTreeName, theseFiles, fFriendInfo,
-                                                           fEntryList, theseEntries, friendEntries);
+         std::tie(reader, elist) = fTreeView->GetTreeReader(c.start, c.end, fTreeName, theseFiles, fFriendInfo,
+                                                            fEntryList, theseEntries, friendEntries);
          func(*reader);
       };